export class ModelInheritor {
  constructor(model) {
    this.models = resolveModels(model)
    this._cacheMap = {}
  }

  createRunner(thisObj) {
    return new ModelRunner(this, thisObj)
  }

  getApplyLastInfos(key) {
    return this._getCache('applyLastInfos:' + key, () => genApplyLastInfos({ key, jsos: this.models }))
  }

  getFireInfos(key) {
    return this._getCache('fireInfos:' + key, () => this.genFireFuns(key))
  }

  genFireFuns(key) {
    const arr = []
    for (const m of this.models) {
      const fun = m[key]
      if (typeof fun === 'function') {
        arr.push(fun)
      }
    }
    return arr
  }

  getMergeSecondFunData(key) {
    return this._getCache('getMergeSecondFunData:' + key, () => this.genMergeSecondData({ key, ignoreDataType: true }))
  }

  getMergeSecondMixData(key) {
    return this._getCache('getMergeSecondMixData:' + key, () => this.genMergeSecondData({ key }))
  }

  genMergeSecondData({ key, ignoreDataType }) {
    const jsos = []
    const itemKeyMap = {}
    for (const m of this.models) {
      const obj = m[key]
      if (typeof obj === 'object') {
        jsos.push(obj)
        Object.assign(itemKeyMap, obj)
      }
    }

    const result = {}
    for (const itemKey in itemKeyMap) {
      const infos = genApplyLastInfos({ key: itemKey, jsos, ignoreDataType })
      if (infos.length > 0) {
        result[itemKey] = infos
      }
    }

    return result
  }

  _getCache(key, getter) {
    if (!this._cacheMap[key]) {
      this._cacheMap[key] = { key, getter, data: getter() }
    }
    return this._cacheMap[key].data
  }
}

export class ModelRunner {
  constructor(modelInheritor, thisObj) {
    this.modelInheritor = modelInheritor
    this.thisObj = thisObj
    this.thisObj.$runner = this
    this.models = modelInheritor.models

    this.upperQueue = new Queue()
    this.upperName = '$upper'

    this._cacheMap = {}
  }

  applyLast(key, datas = []) {
    const fun = this._getCache('applyLast:' + key, () =>
      this._genApplyLastFun(this.modelInheritor.getApplyLastInfos(key))
    )
    return fun && fun(...datas)
  }

  fire(key, datas) {
    const funs = this.modelInheritor.getFireInfos(key)
    for (const fun of funs) {
      fun.apply(this.thisObj, datas)
    }
  }
  async fireAwait(key, datas) {
    const funs = this.modelInheritor.getFireInfos(key)
    for (const fun of funs) {
      await fun.apply(this.thisObj, datas)
    }
  }

  // 只合并值
  mergeSecondValue(key) {
    const result = {}
    for (const m of this.models) {
      const obj = m[key]
      if (typeof obj === 'object') {
        Object.assign(result, obj)
      } else if (typeof obj === 'function') {
        Object.assign(result, obj.apply(this.thisObj))
      }
    }
    return result
  }

  // 只合并fun
  mergeSecondFun(key) {
    const result = {}
    const keyToInfos = this.modelInheritor.getMergeSecondFunData(key)
    for (const key in keyToInfos) {
      result[key] = this._genApplyLastFun(keyToInfos[key])
    }
    return result
  }

  // 合并函数和值
  mergeSecondMix(key) {
    const result = {}
    const keyToInfos = this.modelInheritor.getMergeSecondMixData(key)
    for (const key in keyToInfos) {
      const fun = this._genApplyLastFun(keyToInfos[key])
      result[key] = fun.isDataType ? fun() : fun
    }
    return result
  }

  _genApplyLastFun(infos) {
    let upper
    let curr = null
    for (const info of infos) {
      if (info.fun) {
        curr = this._genHasUpperFun(info.fun, upper)
      } else {
        curr = () => info.data
        curr.isDataType = true
      }
      curr._upper = upper
      upper = curr
    }
    return curr
  }

  _genHasUpperFun(fun, upper) {
    const thisObj = this.thisObj
    fun = thisObj ? fun.bind(thisObj) : fun
    upper = thisObj ? upper : null
    const result = !upper
      ? (...datas) => fun(...datas)
      : (...datas) => {
        try {
          thisObj[this.upperName] = this.upperQueue.push(upper)
          return fun(...datas)
        } finally {
          thisObj[this.upperName] = this.upperQueue.next()
        }
      }
    result._source = fun
    return result
  }

  _getCache(key, getter) {
    if (!this._cacheMap[key]) {
      this._cacheMap[key] = { key, getter, data: getter() }
    }
    return this._cacheMap[key].data
  }
}

function resolveModels(model) {
  const shared = {}
  let rawModels = loopRawModels(model, shared)

  // 去重（主要针对没有name的情况）
  rawModels = Array.from(new Set(rawModels))

  shared.mergeSeconds = []
  const eachModel = (m) => Array.isArray(m.mergeSeconds) && shared.mergeSeconds.push(...m.mergeSeconds)
  rawModels.forEach((m) => (ModelMergeBody.is(m) ? m.each(eachModel) : eachModel(m)))
  shared.mergeSeconds = Array.from(new Set(shared.mergeSeconds))
  const resultModels = rawModels.map((m) => (ModelMergeBody.is(m) ? m.merge() : m))

  return resultModels
}

function loopRawModels(model, shared = {}) {
  let etds = model.extends
  etds = etds instanceof Array ? [...etds] : typeof etds === 'object' ? [etds] : []

  const models = []
  for (const item of etds) {
    models.push(...loopRawModels(item, shared))
  }
  models.push(model)

  return mergeSameNameModel(models, shared)
}

function mergeSameNameModel(models, shared) {
  let newModels = []
  const nameToModelMap = {}

  for (const model of models) {
    const name = model.name
    if (name) {
      // 只保存第一个；保存名字，方便后续替换优先级最高的对象
      if (!nameToModelMap[name]) {
        newModels.push(name)
      }
      // 如果 modelMerge is true ,表示当前model会之前的合并，而不是替换
      if (model.modelMerge) {
        if (nameToModelMap[name]) {
          nameToModelMap[name] = new ModelMergeBody(shared, nameToModelMap[name], model)
        } else {
          nameToModelMap[name] = model
        }
      } else {
        nameToModelMap[name] = model
      }
    } else {
      newModels.push(model)
    }
  }

  for (const idx in newModels) {
    const m = newModels[idx]
    if (typeof m === 'string') {
      newModels[idx] = nameToModelMap[m]
    }
  }
  return newModels
}

// ----------------------

class Queue {
  constructor() {
    this.arr = []
  }
  push(item) {
    this.arr.push(item)
    return item
  }
  pop() {
    return this.arr.pop()
  }
  last() {
    return this.arr[this.arr.length - 1]
  }
  next() {
    this.pop()
    return this.last()
  }
}

function genApplyLastInfos({ key, jsos, ignoreDataType = false }) {
  const arr = []
  const len = jsos.length
  for (let index = len - 1; index >= 0; index--) {
    const jso = jsos[index]

    if (key in jso) {
      const obj = jso[key]
      if (typeof obj === 'function') {
        arr.unshift({ fun: obj })
      } else {
        if (ignoreDataType) continue
        arr.unshift({ data: obj })
        break
      }
    }
  }
  return arr
}

class ModelMergeBody {
  constructor(shared, parent, child) {
    this.name = parent.name
    this.modelMerge = parent.modelMerge
    this.shared = shared
    this.parent = parent
    this.child = child
  }

  each(fun) {
    ModelMergeBody.is(this.parent) ? this.parent.each(fun) : fun(this.parent)
    ModelMergeBody.is(this.child) ? this.child.each(fun) : fun(this.child)
  }

  merge() {
    const _parent = ModelMergeBody._applyModel(this.parent)
    const _child = ModelMergeBody._applyModel(this.child)
    const result = { ..._parent, ..._child }
    for (const secondKey of this.shared.mergeSeconds) {
      result[secondKey] = { ...drawObj(_parent[secondKey]), ...drawObj(_child[secondKey]) }
    }
    return result
  }

  static _applyModel(obj) {
    return ModelMergeBody.is(obj) ? obj.merge() : obj
  }

  static is(obj) {
    return obj instanceof ModelMergeBody
  }
}

// ----------------------
function drawObj(val) {
  return typeof val === 'object' ? val : undefined
}
